<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>5.3.3-如何代理 Object</title>
</head>

<body>
    <div id="container"></div>
</body>
<script >
    function* EnumerateObjectProperties(obj) { // 新增 EnumerateObjectProperties 函数
        const visited = new Set();
        for (const key of Reflect.ownKeys(obj)) {
            if (typeof key === "symbol") continue;
            const desc = Reflect.getOwnPropertyDescriptor(obj, key);
            if (desc) {
                visited.add(key);
                if (desc.enumerable) yield key;
            }
        }
        const proto = Reflect.getPrototypeOf(obj);
        if (proto === null) return;
        for (const protoKey of EnumerateObjectProperties(proto)) {
            if (!visited.has(protoKey)) yield protoKey;
        }
    }
</script>
<script>
    /**
     * 如何拦截 for...in 循环？
     * - 任何操作其实都是由这些基本语义方法及其组合实现的，for...in 循环也不例外
     * 
     * - 规范的 14.7.5.6节中定义了 for...in 头部的执行规则
     * -- 第 6 步的第 c 子步骤,让 iterator 的值为 ? EnumerateObjectProperties(obj)。
     * --- 关键点在于 EnumerateObjectProperties(obj)。
     *      这里的 EnumerateObjectProperties 是一个抽象方法，
     *      该方法返回一个迭代器对象
     * --- 实际上，obj 就是被 for...in 循环遍历的对象，
     *      其关键点在于使用 Reflect.ownKeys(obj) 来获取只属于对象自身拥有的键
     * --- 可以使用 ownKeys 拦截函数来拦截 Reflect.ownKeys
     * 
     * 
     * 将ITERATE_KEY 作为追踪的 key，为什么这么做呢？
     * - 在 set/get 中，我们可以得到具体操作的key， 
     *    但是在 ownKeys 中，我们只能拿到目标对象 target
     * - ownKeys 用来获取一个对象的所有属于自己的键值
     * - 这个操作明显不与任何具体的键进行绑定，
     *    因此我们只能够构造唯一的 key 作为标识，即 ITERATE_KEY
     * 
     * 为对象 p 添加新的属性 bar 时，没有触发副作用函数重新执行，这是为什么呢？
     * - 当为对象 p 添加新的 bar 属性时，会触发 set 拦截函数执行
     * - 此时 set 拦截函数接收到的 key 就是字符串 'bar'，
     *    因此最终调用 trigger 函数时也只是触发了与 'bar' 相关联的副作用函数重新执行
     * - for...in 循环是在副作用函数与 ITERATE_KEY 之间建立联系，这和'bar' 一点儿关系都没有，
     *    因此当我们尝试执行 p.bar = 2 操作时，并不能正确地触发响应
     * 
     * 在什么情况下，对数据的操作需要触发与 ITERATE_KEY 相关联的副作用函数重新执行呢？
     * - 当为对象添加新属性时，
     *    除了把那些直接与具体操作的key 相关联的副作用函数取出来执行外，
     *    还要把那些与 ITERATE_KEY 相关联的副作用函数取出来执行。
     * 
     * */
    const bucket = new WeakMap();
    const ITERATE_KEY = Symbol(); // 新增 ITERATE_KEY 常量

    // 在 get 拦截函数内调用 track 函数追踪变化
    function track(target, key) {
        // 没有 activeEffect，直接 return
        if (!activeEffect) return
        let depsMap = bucket.get(target)
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) {
            depsMap.set(key, (deps = new Set()))
        }
        // 把当前激活的副作用函数添加到依赖集合 deps 中
        deps.add(activeEffect)
        // deps 就是一个与当前副作用函数存在联系的依赖集合
        // 将其添加到 activeEffect.deps 数组中
        activeEffect.deps.push(deps) 
    }
    // 在 set 拦截函数内调用 trigger 函数触发变化
    function trigger(target, key) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        // 取得与 key 相关联的副作用函数
        const effects = depsMap.get(key)
        // 取得与 ITERATE_KEY 相关联的副作用函数
        const iterateEffects = depsMap.get(ITERATE_KEY); // 新增

        const effectsToRun = new Set()
        // 将与 key 相关联的副作用函数添加到 effectsToRun
        effects && effects.forEach(effectFn => {
            // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同，则不触发执行
            if (effectFn !== activeEffect) {  
                effectsToRun.add(effectFn)
            }
        })
        // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
        iterateEffects && iterateEffects.forEach(effectFn => { // 新增
            if (effectFn !== activeEffect) {  
                effectsToRun.add(effectFn)
            }
        })

        effectsToRun.forEach(effectFn => {
            // 如果一个副作用函数存在调度器，则调用该调度器，并将副作用函数作为参数传递
            if (effectFn.options.scheduler) {  
                effectFn.options.scheduler(effectFn)  
            } else {
                // 否则直接执行副作用函数（之前的默认行为）
                effectFn() 
            }
        })
    }

    // 用一个全局变量存储被注册的副作用函数
    let activeEffect
    // effect 栈
    // 个人理解，始终都会留着一个是最先执行的
    const effectStack = []  
    function effect(fn, options = {}) {
        const effectFn = () => {
            cleanup(effectFn)
            // 当调用 effect 注册副作用函数时，将副作用函数赋值给 activeEffect
            activeEffect = effectFn
            // 在调用副作用函数之前将当前副作用函数压栈
            effectStack.push(effectFn)
            // 将 fn 的执行结果存储到 res 中
            const res = fn()  
            // 在当前副作用函数执行完毕后，将当前副作用函数弹出栈，并把 activeEffect 还原为之前的值
            effectStack.pop()
            activeEffect = effectStack[effectStack.length - 1]
            return res  
        }
        // 将 options 挂载到 effectFn 上
        effectFn.options = options 
        // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
        effectFn.deps = []
        // 只有非 lazy 的时候，才执行
        if (!options.lazy) {  
            // 执行副作用函数
            effectFn()
        }
        // 将副作用函数作为返回值返回
        return effectFn 
    }

    function cleanup(effectFn) {
        // 遍历 effectFn.deps 数组
        for (let i = 0; i < effectFn.deps.length; i++) {
            // deps 是依赖集合
            const deps = effectFn.deps[i]
            // 将 effectFn 从依赖集合中移除
            deps.delete(effectFn)
        }
        // 最后需要重置 effectFn.deps 数组
        effectFn.deps.length = 0
    }

    const obj = { foo: 1 }

    const p = new Proxy(obj, {
        get(target, key, receiver) {
            // 建立联系
            track(target, key)
            // 返回属性值
            return Reflect.get(target, key, receiver)
        },
        // 拦截设置操作
        set(target, key, newVal, receiver) {
            // 设置属性值
            const res = Reflect.set(target, key, newVal, receiver) // 新增
            // 把副作用函数从桶里取出并执行
            trigger(target, key)
            return res;
        },
        // 拦截 in 操作符
        has(target, key) {
            // 建立联系
            track(target, key)
            // 返回属性值
            return Reflect.has(target, key)
        },
        // 拦截for...in循环
        ownKeys(target) { // 新增
            // 将副作用函数与 ITERATE_KEY 关联
            track(target, ITERATE_KEY)
            return Reflect.ownKeys(target)
        },
    })

    effect(() => {
        // for...in 循环
        for (const key in p) {
            console.log(key) // foo
        }
    })

    setTimeout(() => {
        p.bar = 2
    }, 1000)

</script>

</html>